package gdma.hqdes.stat.renderer;

import com.deepoove.poi.data.ChartMultiSeriesRenderData;
import com.deepoove.poi.data.SeriesRenderData;
import common.exception.BadRequestException;
import common.util.ListUtils;
import common.util.TreeUtils;
import gdma.hqdes.bean.*;
import gdma.hqdes.service.IndexService;
import gdma.hqdes.service.ProjectService;
import gdma.hqdes.service.UnitService;
import gdma.hqdes.stat.bean.ChartMultiSeriesRenderDataEx;
import gdma.hqdes.stat.bean.IndexCombo;
import gdma.hqdes.stat.bean.StatQuery;
import io.vavr.Tuple;
import io.vavr.Tuple2;
import org.apache.poi.xddf.usermodel.chart.XDDFAreaChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFBarChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFLineChartData;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @Author wangyue
 * @Version 3.0
 * @Date 2024-07-29
 */
@Service
public class UnitScoreDataRenderer {

    @Autowired
    ProjectService projectService;

    @Autowired
    IndexService indexService;

    @Autowired
    UnitService unitService;

    public ChartMultiSeriesRenderData renderIndexes(int year, int scope, Long... indexIds) {
        HqdProject prj = getProject(year, scope);
        List<HqdIndex> idxes = indexService.getThinIndexes(indexIds);
        if(CollectionUtils.isEmpty(idxes)) return null;
        List<UnitStat> unitStats = unitService.statUnitScores(prj, null, idxes.stream().map(HqdIndex::getId).collect(Collectors.toList()));
        ChartMultiSeriesRenderData result = buildChartMultiSeriesRenderData(unitStats, idxes);
        result.setChartTitle(idxes.stream().map(HqdIndex::getName).collect(Collectors.joining("/")));
        return result;
    }

    private HqdProject getProject(int year, int scope) {
        HqdProject prj = projectService.getProject(year, scope);
        if(prj == null) throw new BadRequestException("项目未找到（" + year + "年" + Unit.SCOPE_TEXTS.get(scope) + "），请确保项目已创建");
        return prj;
    }

    private static ChartMultiSeriesRenderData buildChartMultiSeriesRenderData(List<UnitStat> unitStats, List<HqdIndex> idxes) {
        ChartMultiSeriesRenderData result = new ChartMultiSeriesRenderDataEx();
        result.setChartTitle("");
        result.setCategories(unitStats.stream().map(UnitStat::getShortName).toArray(String[]::new));
        List<SeriesRenderData> seriesRenderDatas = new ArrayList<>();
        for(int i = 0; i < idxes.size(); i++) {
            HqdIndex idx = idxes.get(i);
            final int pos = i;
            Number[] data = unitStats.stream().map(stat -> {
                Map<Long, UnitScore> scoreMap = stat.getScoreMap();
                UnitScore score = scoreMap.get(idx.getId());
                if(score != null) return score.getScore();
                return 0f;
            }).toArray(Float[]::new);
            SeriesRenderData seriesRenderData = new SeriesRenderData(idx.getName(), data);
            seriesRenderData.setComboType(SeriesRenderData.ComboType.BAR);
            seriesRenderDatas.add(seriesRenderData);
        }
        result.setSeriesDatas(seriesRenderDatas);
        return result;
    }

    public ChartMultiSeriesRenderData renderChildIndexes(int year, int scope, long parentIndexId) {
        HqdProject prj = getProject(year, scope);
        HqdIndex root = indexService.buildThinIndexTree(prj.getIndexVerId());
        HqdIndex idx = TreeUtils.find(root, n -> n.getId().equals(parentIndexId));
        List<HqdIndex> idxes = idx.getChildren();
        if(CollectionUtils.isEmpty(idxes)) return null;
        List<UnitStat> unitStats = unitService.statUnitScores(prj, null, idxes.stream().map(HqdIndex::getId).collect(Collectors.toList()));
        ChartMultiSeriesRenderData result = buildChartMultiSeriesRenderData(unitStats, idxes);
        result.setChartTitle(idx.getName());
        return result;
    }

    public ChartMultiSeriesRenderData renderIndexAncestry(int year, int scope, long endIndexId, int maxLength, XWPFChart placeheldChart) {
        HqdProject prj = getProject(year, scope);
        HqdIndex root = indexService.buildThinIndexTree(prj.getIndexVerId());
        HqdIndex idx = TreeUtils.find(root, n -> n.getId().equals(endIndexId));
        List<HqdIndex> idxes = new ArrayList<>();
        while(idx != null) {
            idxes.add(idx);
            idx = idx.getParent();
        }
        idxes = idxes.subList(0, maxLength < idxes.size() ? maxLength : idxes.size());
        List<UnitStat> unitStats = unitService.statUnitScores(prj, null, idxes.stream().map(HqdIndex::getId).collect(Collectors.toList()));
        ChartMultiSeriesRenderData result = buildChartMultiSeriesRenderData(unitStats, idxes);
        result.setChartTitle(idxes.get(0).getName());
        if(placeheldChart != null) adjustComboTypes(result, placeheldChart);
        return result;
    }

    public static void adjustComboTypes(ChartMultiSeriesRenderData result, XWPFChart placeheldChart) {
        int i = 0;
        for(XDDFChartData chartData : placeheldChart.getChartSeries()) {
            SeriesRenderData seriesData = ListUtils.getOrNull(result.getSeriesDatas(), i++);
            if(seriesData == null) continue;
            if(chartData.getSeriesCount() > 0) {
                XDDFChartData.Series series = chartData.getSeries(0);
                if(series instanceof XDDFBarChartData.Series) {
                    seriesData.setComboType(SeriesRenderData.ComboType.BAR);
                } else if(series instanceof XDDFLineChartData.Series) {
                    seriesData.setComboType(SeriesRenderData.ComboType.LINE);
                } else if(series instanceof XDDFAreaChartData.Series) {
                    seriesData.setComboType(SeriesRenderData.ComboType.AREA);
                }
            }
        }
    }

    public ChartMultiSeriesRenderData renderIndexCombos(int year, int scope, List<String> unitCombos, List<IndexCombo> indexCombos) {
        Set<Long> unitIds = fetchUnitIds(unitCombos, scope);
        HqdProject prj = getProject(year, scope);
        Set<Long> idxIds = new HashSet<>();
        for(IndexCombo combo : indexCombos) {
            Object idxObj = combo.getIndexObject();
            if(idxObj instanceof HqdIndex) {
                idxIds.add(((HqdIndex)idxObj).getId());
            } else if(idxObj instanceof StatIndex) {
                StatIndex statIndex = (StatIndex)idxObj;
                if(statIndex.getItems() == null) statIndex.setItems(indexService.getStatIndexItems(statIndex.getId()));
                for(StatIndexItem item : statIndex.getItems()) {
                    idxIds.add(item.getIndexId());
                }
            }
        }
        List<UnitStat> unitStats = unitService.statUnitScores(prj, unitIds, idxIds);
        unitStats = aggregateAndFilter(unitStats, unitCombos);
        List<Tuple2<UnitStat, List<Float>>> tuples = tuplize(unitStats, indexCombos);
        sortTuples(tuples, indexCombos);
        return buildComboChartMultiSeriesRenderData(tuples, indexCombos);
    }

    private Set<Long> fetchUnitIds(List<String> unitCombos, int scope) {
        Set<Long> unitIds = new HashSet<>();
        for(String unitCombo : unitCombos) {
            try {
                unitIds.add(Long.parseLong(unitCombo));
            } catch(Exception x) {
                Map<String, List<Unit>> areaUnitsMap = unitService.getAreaUnitsMap(scope);
                if(areaUnitsMap != null) {
                    List<Unit> areaUnits = areaUnitsMap.get(unitCombo);
                    if(!CollectionUtils.isEmpty(areaUnits))
                        unitIds.addAll(areaUnits.stream().map(Unit::getId).collect(Collectors.toList()));
                }
            }
        }
        return unitIds;
    }

    private List<UnitStat> aggregateAndFilter(List<UnitStat> unitStats, List<String> unitCombos) {
        Map<Long, UnitStat> unitStatMap = unitStats.stream().collect(Collectors.toMap(UnitStat::getId, Function.identity()));
        List<UnitStat> result = new ArrayList<>();
        for(String unitCombo : unitCombos) {
            try {
                Long unitId = Long.parseLong(unitCombo);
                UnitStat unitStat = unitStatMap.get(unitId);
                if(unitStat != null) result.add(unitStat);
            } catch(Exception x) {
                UnitStat unitStat = new UnitStat();
                unitStat.setAreaTag(unitCombo);
                unitStat.setShortName(Unit.AREA_TEXTS.get(unitCombo));
                List<UnitStat> areaStat = unitStats.stream().filter(us -> unitCombo.equals(us.getAreaTag())).collect(Collectors.toList());
                Map<Long, Double> averageScores = areaStat.stream()
                        .flatMap(stat -> stat.getScores().stream())
                        .collect(Collectors.groupingBy(UnitScore::getIndexId, Collectors.averagingDouble(UnitScore::getScore)));
                List<UnitScore> averagedScores = averageScores.entrySet().stream()
                        .map(e -> {
                            UnitScore unitScore = new UnitScore();
                            unitScore.setIndexId(e.getKey());
                            unitScore.setScore(e.getValue().floatValue());
                            return  unitScore;
                        }).collect(Collectors.toList());
                unitStat.setScores(averagedScores);
                result.add(unitStat);
            }
        }
        return result;
    }

    private List<Tuple2<UnitStat, List<Float>>> tuplize(List<UnitStat> unitStats, List<IndexCombo> indexCombos) {
        return unitStats.stream().map(stat -> {
            List<Float> comboScores = new ArrayList<>();
            for(IndexCombo combo : indexCombos) {
                Map<Long, UnitScore> scoreMap = stat.getScoreMap();
                if(combo.getIndexObject() instanceof HqdIndex) {
                    HqdIndex idx = (HqdIndex)combo.getIndexObject();
                    UnitScore score = scoreMap.get(idx.getId());
                    comboScores.add(score != null ? score.getScore() : 0f);
                } else if(combo.getIndexObject() instanceof StatIndex) {
                    StatIndex statIndex = (StatIndex)combo.getIndexObject();
                    float weightTotal = 0, scoreTotal = 0f;
                    for(StatIndexItem item : statIndex.getItems()) {
                        UnitScore score = scoreMap.get(item.getIndexId());
                        if(item.getWeight() == null) item.setWeight(100f / statIndex.getItems().size());
                        weightTotal += item.getWeight();
                        if(score != null) scoreTotal += score.getScore() * item.getWeight();
                    }
                    comboScores.add(weightTotal > 0 ? scoreTotal / weightTotal : 0f);
                }
            }
            return Tuple.of(stat, comboScores);
        }).collect(Collectors.toList());
    }

    private static <T extends Stat> List<Tuple2<T, List<Float>>> sortTuples(List<Tuple2<T, List<Float>>> tuples, List<IndexCombo> indexCombos) {
        int len = indexCombos.size();
        for(int i = 0; i < len; i++) {
            IndexCombo indexCombo = indexCombos.get(i);
            if(indexCombo.getDescOrder() == null) indexCombo.setDescOrder(len + i);
        }
        int[] descOrders = IntStream.range(0, indexCombos.size())
                .boxed()
                .sorted(Comparator.comparingInt(i -> indexCombos.get(i).getDescOrder()))
                .mapToInt(Integer::intValue)
                .toArray();
        tuples.sort((a, b) -> {
            for(int i : descOrders) {
                Float f1 = a._2().get(i);
                Float f2 = b._2().get(i);
                int r = f1.compareTo(f2);
                if(r != 0) return -r;
            }
            return 0;
        });
        return tuples;
    }

    private static <T extends Stat> ChartMultiSeriesRenderData buildComboChartMultiSeriesRenderData(List<Tuple2<T, List<Float>>> tuples, List<IndexCombo> indexCombos) {
        ChartMultiSeriesRenderDataEx result = new ChartMultiSeriesRenderDataEx();
        List<SeriesRenderData> seriesRenderDatas = new ArrayList<>();
        result.setCategories(tuples.stream().map(t -> t._1.toShortString()).toArray(String[]::new));
        for(int i = 0; i < indexCombos.size(); i++) {
            IndexCombo combo = indexCombos.get(i);
            SeriesRenderData seriesRenderData = new SeriesRenderData();
            int finalI = i;
            Number[] values = tuples.stream().map(tuple -> tuple._2().get(finalI)).toArray(Float[]::new);
            seriesRenderData.setName(combo.getIndexObject().getName());
            seriesRenderData.setValues(values);
            seriesRenderData.setComboType(combo.getComboType());
            seriesRenderDatas.add(seriesRenderData);
        }
        result.setSeriesDatas(seriesRenderDatas);
        result.addCategoryMeta("areaText", tuples.stream()
                .map(t -> t._1 instanceof UnitStat ? ((UnitStat)t._1).getAreaText() : null)
                .collect(Collectors.toList()));
        result.calcSeriesAverages();
        return result;
    }

    public ChartMultiSeriesRenderData renderHistoryIndexCombos(int year0, int year1, int scope, Collection<Long> unitIds, List<IndexCombo> sampleIndexCombos) {
        List<HqdProject> projects = projectService.listProjects(year0, year1, scope, null, null);
        Set<Integer> verIds = projects.stream().map(HqdProject::getIndexVerId).collect(Collectors.toSet());
        Set<String> idxCodes = new HashSet<>();
        Set<String> sIdxCodes = new HashSet<>();
        for(IndexCombo combo : sampleIndexCombos) {
            Object idxObj = combo.getIndexObject();
            if(idxObj instanceof HqdIndex) {
                idxCodes.add(((HqdIndex)idxObj).getCode());
            } else if(idxObj instanceof StatIndex) {
                sIdxCodes.add(((StatIndex)idxObj).getCode());
            }
        }
        Map<Integer, Map<String, Long>> idxMap = indexService.listThinIndexes(verIds, idxCodes, null).stream()
                .collect(Collectors.groupingBy(HqdIndex::getVerId, Collectors.toMap(HqdIndex::getCode, HqdIndex::getId)));
        Set<Long> idxIds = idxMap.values().stream()
                .flatMap(m -> m.values().stream())
                .collect(Collectors.toSet());
        Map<Integer, Map<String, List<StatIndexItem>>> sIdxMap = indexService.listStatIndexes(verIds, sIdxCodes, null).stream()
                .collect(Collectors.groupingBy(StatIndex::getVerId, Collectors.toMap(StatIndex::getCode, s -> s.getItems().stream().collect(Collectors.toList()))));
        Set<Long> sIdxIds = sIdxMap.values().stream()
                .flatMap(innerMap -> innerMap.values().stream())
                .flatMap(List::stream)
                .map(StatIndexItem::getIndexId)
                .collect(Collectors.toSet());
        idxIds.addAll(sIdxIds);
        Set<Long> prjIds = projects.stream().map(HqdProject::getId).collect(Collectors.toSet());
        List<YearStat> yearStats = unitService.statHistoryUnitScores(prjIds, unitIds, idxIds);
        List<Tuple2<YearStat, List<Float>>> tuples = tuplize(yearStats, sampleIndexCombos, idxMap, sIdxMap);
        sortTuples(tuples, sampleIndexCombos);
        return buildComboChartMultiSeriesRenderData(tuples, sampleIndexCombos);
    }

    private List<Tuple2<YearStat, List<Float>>> tuplize(List<YearStat> yearStats, List<IndexCombo> indexCombos, Map<Integer, Map<String, Long>> idxMap, Map<Integer, Map<String, List<StatIndexItem>>> sIdxMap) {
        return yearStats.stream().map(stat -> {
            List<Float> comboScores = new ArrayList<>();
            for(IndexCombo combo : indexCombos) {
                Map<Long, UnitScore> scoreMap = stat.getScoreMap();
                if(combo.getIndexObject() instanceof HqdIndex) {
                    HqdIndex idx0 = (HqdIndex)combo.getIndexObject();
                    Long idxId = idxMap.get(stat.getIndexVerId()).get(idx0.getCode());
                    UnitScore score = scoreMap.get(idxId);
                    comboScores.add(score != null ? score.getScore() : 0f);
                } else if(combo.getIndexObject() instanceof StatIndex) {
                    StatIndex sIdx0 = (StatIndex)combo.getIndexObject();
                    float weightTotal = 0, scoreTotal = 0f;
                    List<StatIndexItem> items = sIdxMap.get(stat.getIndexVerId()).get(sIdx0.getCode());
                    if(items != null) {
                        for(StatIndexItem item : items) {
                            UnitScore score = scoreMap.get(item.getIndexId());
                            if(item.getWeight() == null) item.setWeight(100f / items.size());
                            weightTotal += item.getWeight();
                            if(score != null) scoreTotal += score.getScore() * item.getWeight();
                        }
                    }
                    comboScores.add(weightTotal > 0 ? scoreTotal / weightTotal : 0f);
                }
            }
            return Tuple.of(stat, comboScores);
        }).collect(Collectors.toList());
    }

    public static Map<String, Number> convertToMap(ChartMultiSeriesRenderData chartData) {
        Map<String, Number> resultMap = new HashMap<>();
        String[] categories = chartData.getCategories();
        Number[] values = chartData.getSeriesDatas().get(0).getValues();

        for (int i = 0; i < categories.length; i++) {
            resultMap.put(categories[i], values[i]);
        }
        return resultMap;
    }

    public ChartMultiSeriesRenderData renderHeatmap(StatQuery query) {
        return renderIndexCombos(query.getYear(), query.getScope(), query.getUnitCombos(), query.getIndexCombos());
    }

    public ChartMultiSeriesRenderData renderIndexCombos(StatQuery query) {
        return renderIndexCombos(query.getYear(), query.getScope(), query.getUnitCombos(), query.getIndexCombos());
    }

    public ChartMultiSeriesRenderData renderHistoryIndexCombos(StatQuery query) {
        return renderHistoryIndexCombos(query.getYear(), query.getYear1(), query.getScope(), query.getUnitIds(), query.getIndexCombos());
    }

}
